package com.coderman.common.starter.idempotent.service.impl;

import com.coderman.common.starter.idempotent.service.DistributedLocker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Description:
 * date: 2020/9/16 4:31 下午
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */
@Service(value = "redisLuaScriptLockImpl")
public class RedisLuaScriptLockImpl implements InitializingBean, DistributedLocker {
    private Logger logger = LoggerFactory.getLogger(RedisLuaScriptLockImpl.class);

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 锁脚本
    private DefaultRedisScript<Long> lockScript;
    // 解锁脚本
    private DefaultRedisScript<Long> unlockScript;


    private ThreadLocal<String> threadKeyId = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return UUID.randomUUID().toString().replace("-", "");
        }
    };


    @Override
    public void unlock(String lockKey) throws Exception {
        List<String> keyList = new ArrayList<String>();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        Long info = redisTemplate.execute(unlockScript, keyList);
        if (info == null) {
            logger.error("释放锁失败，lockKey:{}", lockKey);
        } else {
            logger.debug("成功释放锁，lockKey:{}", lockKey);
        }
    }

    @Override
    public boolean lock(final String lockKey, final int timeout, final int tryCount, int sleepTime) throws Exception {
        //默认睡眠200ms
        if (sleepTime == 0) {
            sleepTime = 200;
        }
        List<String> keyList = new ArrayList<String>();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        int sleep = sleepTime;
        //获取锁次数
        int lockTryCount = 0;
        logger.debug("加锁成功，lockKey:{},准备执行业务操作", lockKey);
        while (true) {
            if (tryCount > 0 && lockTryCount > tryCount) {
                //加锁失败超过设定重试次数，抛出异常表示加锁失败
                throw new RuntimeException("access to distributed lock more than retries：" + tryCount);
            }
            if (lockTryCount > 0) {
                logger.debug("重试获取锁:{}操作:{}次", lockKey, lockTryCount);
            }
            boolean result = redisTemplate.execute(lockScript, keyList, String.valueOf(timeout * 1000)) > 0;
            if (result) {
                //返回结果大于0，表示加锁成功
                logger.debug("加锁成功，lockKey:{},准备执行业务操作", lockKey);
                return true;
            } else {
                try {
                    //如果重试失败则睡眠增加50ms,递增累加休眠时间
                    if (lockTryCount > 1) {
                        sleep = sleep + 50;
                    }
                    Thread.sleep(sleep, (int) (Math.random() * 500));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            lockTryCount++;
        }
    }

    @Override
    public boolean lock(String lockKey, int timeout) {
        List<String> keyList = new ArrayList<String>();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        boolean result = redisTemplate.execute(lockScript, keyList,timeout) > 0;
        if (result) {
            //返回结果大于0，表示加锁成功
            logger.debug("加锁成功，lockKey:{},准备执行业务操作", lockKey);
            return true;
        } else {
            return false;
        }
    }


    /**
     * 生成操作redis的lua脚本操作实例
     */
    private void initialize() {
        //lock script
        lockScript = new DefaultRedisScript<Long>();
        lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/lock.lua")));
        lockScript.setResultType(Long.class);
        logger.debug("初始化加锁脚本成功:\n:{}", lockScript.getScriptAsString());

        //unlock script
        unlockScript = new DefaultRedisScript<Long>();
        unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/unlock.lua")));
        unlockScript.setResultType(Long.class);
        logger.debug("初始化释放锁脚本成功:\n:{}", unlockScript.getScriptAsString());
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        initialize();
    }
}
